Skip to content

feat: ENABLE_<CLI> toggles + default-deny for Gemini, OpenCode, Hermes - #30

Closed
dgokeeffe wants to merge 1 commit into
mainfrom
feat/cli-toggles
Closed

feat: ENABLE_<CLI> toggles + default-deny for Gemini, OpenCode, Hermes#30
dgokeeffe wants to merge 1 commit into
mainfrom
feat/cli-toggles

Conversation

@dgokeeffe

@dgokeeffe dgokeeffe commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Introduce per-CLI install toggles (ENABLE_CODEX, ENABLE_OPENCODE, ENABLE_GEMINI, mirroring the existing ENABLE_HERMES), and default Gemini, OpenCode, and Hermes to OFF. A fresh CoDA deploy now ships with two CLIs running by default — Claude Code and Codex.

Security posture rationale

The App container holds the rotated workspace PAT (~/.databrickscfg, written by pat_rotator.py every 10 min) and the user's repos under ~/projects/. Any process that runs in there has effective full workspace access. That puts a higher bar on "what installs by default" than for local dev tools.

CLI Default Why
Claude Code (Anthropic) enabled (always-on; not toggleable) Primary CLI; vendor with mature signing / SBOM / advisory disclosure
Codex (OpenAI) enabled by default Same supply-chain tier as Anthropic
Gemini (Google) opt-in Vendor has signing infra but lags upstream on SBOM/advisory pipelines for the CLI specifically
OpenCode (community/anomalyco fork) opt-in No equivalent enterprise supply-chain process
Hermes (Nous Research) opt-in No equivalent enterprise supply-chain process

Operators who want the previous five-CLI experience flip the three toggles back to "true" in their own app.yaml override.

Behavioural change for existing deploys

Operators upgrading to this version see Gemini/OpenCode/Hermes disappear unless they explicitly opt in. This is intentional — see rationale above — but worth calling out to reviewers as the substantive UX delta.

Mechanics

  • 4-line gate at the top of each setup_*.py: read env var, exit 0 if false. Exact pattern reuses setup_hermes.py's existing structure.
  • app.yaml documents all four toggles in one comment block.
  • setup_claude.py is intentionally not gated — it bootstraps ~/projects/ and writes app-wide MCP config that the other CLIs reuse.

Test plan

  • Deploy to a workspace; confirm only Claude + Codex install steps run during boot
  • In app.yaml, set ENABLE_GEMINI=true, redeploy; confirm Gemini installs
  • Same for ENABLE_OPENCODE=true and ENABLE_HERMES=true
  • Run pytest tests/ — toggle-introduction tests should still pass
  • Smoke-test the existing already-enabled flow: launch Claude Code and Codex from the App terminal, verify both authenticate against the workspace

Follow-ups (out of scope for this PR)

  • Flip the Python-level fallback defaults to "false" too, so a missing app.yaml also fails-closed. Currently each setup_*.py reads os.environ.get("ENABLE_X", "true") which is fail-open if app.yaml is absent.
  • Add audit GH Action coverage for the still-enabled CLIs (Codex's @openai/codex is already in dependency-audit.yml; Claude Code is installed via curl so out of npm-audit scope).

This pull request and its description were written by Isaac.

Introduce per-CLI install toggles for Codex, OpenCode, Gemini, Hermes
(replicating the existing ENABLE_HERMES pattern across the other three
secondary CLIs), and default the three less-vetted CLIs to OFF.

Security posture rationale (the substantive change vs. the original
"toggles only" PR): the App container holds the rotated workspace PAT
(~/.databrickscfg, written by pat_rotator.py) and all repos under
~/projects/, so any process that runs there has effective full
workspace access. That makes the supply-chain bar for runtime CLIs
higher than for a local dev tool. Claude Code (Anthropic) and Codex
(OpenAI) ship from vendors with mature signing / SBOM / advisory
pipelines and stay enabled by default. Gemini, OpenCode, and Hermes
don't have equivalent supply-chain processes; they become opt-in.
Operators flip ENABLE_<CLI>=true in app.yaml to install them.

Note that this changes the default user experience: a fresh deploy
ships with two CLIs (Claude, Codex) instead of five. Operators who
want the previous behaviour can flip the three toggles back in their
own app.yaml override.

Mechanics:
- 4-line gate at the top of each setup_*.py: read env var, exit 0 if false
- app.yaml documents all four toggles in one comment block

Claude Code stays always-on — it's the primary CLI and setup_claude.py
also creates ~/projects and writes app-wide MCP config that other agents
can re-use. Disentangling those would expand scope; can be a follow-up.

Smoke-tested locally:
  $ ENABLE_CODEX=false python3 setup_codex.py
  ENABLE_CODEX=false — skipping Codex CLI setup
  $ # exit 0, no further side effects

Follow-ups (out of scope):
- Flip the Python-level defaults to "false" too, so a missing app.yaml
  also fails-closed. Currently each setup_*.py reads
  os.environ.get("ENABLE_X", "true") which is a fail-open default.
- Add an audit GH Action equivalent for the still-enabled CLIs against
  npm advisories (Codex's @openai/codex via dependency-audit.yml is
  already covered).

Co-authored-by: Isaac
@dgokeeffe
dgokeeffe force-pushed the feat/cli-toggles branch from 8d44956 to 513193f Compare May 7, 2026 13:57
@dgokeeffe dgokeeffe changed the title feat: ENABLE_<CLI>=false toggles for Codex / OpenCode / Gemini feat: ENABLE_<CLI> toggles + default-deny for Gemini, OpenCode, Hermes May 7, 2026
dgokeeffe added a commit that referenced this pull request Aug 5, 2026
…rlay

Apps overlays *replace* app.yaml rather than merging with it, and every
setup script reads its toggle as `os.environ.get("ENABLE_<CLI>", "true")`.
So a toggle omitted from an overlay is not a no-op — it silently re-enables
that CLI's install on the deployed container.

app.yaml and app.yaml.workshop listed all five; app.yaml.template and
app.yaml.lakemeter listed only ENABLE_HERMES and ENABLE_PI, so deploys from
those overlays would install Codex and Gemini even though neither has a
compatible gateway endpoint — wasted boot time plus agents that fail on
first request.

Adds tests/test_app_yaml_overlays.py to hold the invariant: every tracked
app.yaml* declares all five toggles, values stay quoted strings (unquoted
`true` parses as a bool, which the scripts' .strip().lower() would choke on),
and a new ENABLE_* appearing in setup_*.py fails the test until it's added
to the overlays.

This is the part of #30 that main hadn't already absorbed: the toggle
mechanism itself landed separately, and main's default-on policy for
Hermes/OpenCode is a deliberate later workshop decision.
@dgokeeffe

Copy link
Copy Markdown
Collaborator Author

The ENABLE_ toggle mechanism is already on main and is a superset of this branch. The remaining gap — app.yaml.template and app.yaml.lakemeter declaring only 2 of the 5 toggles, which matters because overlays replace rather than merge — is addressed in a follow-up PR with a regression test.

@dgokeeffe dgokeeffe closed this Aug 5, 2026
dgokeeffe added a commit that referenced this pull request Aug 5, 2026
…us, app-state, /health (#44)

* fix(cli-auth): atomic writes + observable failures on PAT rotation

Hermes was returning 403 ("Invalid access token") on the first call after
a PAT rotation, then succeeding on retry. Two reasons:

1. update_cli_tokens() rewrote each agent's config file with a bare
   open(path, "w"), creating a window where a concurrent Hermes
   invocation could read a half-written api_key line. Hermes is exposed
   to this because it re-reads ~/.hermes/config.yaml on every call;
   Claude/Codex/Gemini cache the token in env at process startup.
2. Every write path silently swallowed OSError, so an actual write
   failure (perms, locked file, ENOSPC) would leave the config stale
   forever with no log line — the user just saw 403s.

Adds _atomic_write_text() helper (write to .tmp, os.replace) used by
all five _update_* functions. Replaces silent except OSError: pass with
logger.warning at WARNING level. FileNotFoundError still silenced via an
explicit os.path.exists() guard so the rotator doesn't spam during the
brief window between app start and setup script completion.

Co-authored-by: Isaac

* ci(release): generate signed SBOM on each release

Adds supply-chain provenance to every GitHub Release so enterprise
security teams (PCI-DSS / ISO 27001 / APRA CPS 234) can verify what
shipped and prove it came from this repo's workflow.

What's attached to each release now:
- coda-sbom.cdx.json — CycloneDX SBOM (Python + npm deps via syft)
- coda-sbom.cdx.json.cosign.bundle — cosign keyless signature bundle
  (cert + signature + Rekor inclusion proof)

Signing uses GitHub OIDC — no long-lived keys. The signing identity is
anchored to this workflow path and the release tag, and a public
transparency-log entry is recorded in Rekor.

Workflow changes:
- Added `id-token: write` permission (required for OIDC keyless signing)
- Added anchore/sbom-action step (SHA-pinned, format=cyclonedx-json)
- Added sigstore/cosign-installer + sign-blob + in-workflow verify
- Extended softprops/action-gh-release `files:` to attach both artefacts

README changes:
- New "Verifying release provenance" subsection with the cosign
  verify-blob command operators can run.

Co-authored-by: Isaac

* docs(legal): add Project Support / AS-IS disclaimer to README

databrickslabs/dqx and other Labs projects ship a prominent "Project
Support" block disclaiming SLAs and pointing users at the LICENSE for
binding terms. CoDA has the LICENSE.md (with $1,000 aggregate liability
cap + comprehensive warranty disclaimers) and NOTICE.md (full
third-party attribution), but the README didn't surface either.

Without this block, a user evaluating CoDA could plausibly assume it's
a supported Databricks product because it lives under the
databrickslabs/ org. The disclaimer closes that gap — and is placed
immediately after the tagline (not buried at the bottom) because the
tool exposes a workspace-credential-scoped shell and AI agents that
act with full user authority.

Text lifted from databrickslabs/dqx with one grammar tweak (singular
"It is provided AS-IS" since this is one project, not multiple).
Cross-references LICENSE.md and NOTICE.md so security-conscious readers
can find the binding legal text in one hop.

Co-authored-by: Isaac

* fix(security): close unauth info-disclosure surface (setup-status, pat-status, app-state, /health)

Closes the three P3/P4 info-disclosure findings from the 2026-05-17
pen test:

1. /api/setup-status was auth-exempt and returned the full setup_state
   dict including step error messages (stderr trimmed to 500 chars). If a
   setup script failed with leaky stderr — paths, version strings, env-var
   echoes — anyone hitting the URL could read it.

2. /api/pat-status was auth-exempt and returned workspace_host. Already
   known to anyone who reached the URL (they're auth'd to the same
   workspace), but principle-of-least-info says trim it.

3. /api/app-state was auth-exempt and returned app_owner email + last
   PAT rotation timestamp. PR #40 also removes this — including the same
   one-line fix here lets it ship sooner; PR #40 will rebase cleanly.

4. /health was auth-exempt and returned version, active_sessions,
   setup_status, session_timeout_seconds. Version exposure enables
   CVE-targeted exploit selection. Trimmed to {"status": "healthy"} —
   no internal or external caller needs the rich shape (the frontend
   doesn't poll /health; the Apps platform uses its own liveness
   mechanism upstream of the app).

Removed setup-status, pat-status, and app-state from the
before_request auth-exempt list. The frontend continues to poll all
three because it loads from / (auth'd), so it already has SSO cookies.

Test changes:
- 5 new tests in TestInfoDisclosureEndpoints covering: setup-status
  denied/allowed, pat-status denied, app-state denied, /health
  minimal-response anti-leak (explicit assertions that version,
  setup_status, active_sessions are NOT in the response).
- Updated test_session_linger.py: the 24h timeout assertion now reads
  SESSION_TIMEOUT_SECONDS from the module directly instead of via
  /health, since exposing the value to unauth callers was the leak
  we're closing. Stronger test, smaller attack surface.

236/236 unit tests pass.

Co-authored-by: Isaac

* docs: move SBOM verification to docs/SECURITY.md, drop stale plans

Keeps the README lean — release-provenance details now live in
docs/SECURITY.md where security reviewers expect them. Also removes
docs/plans/, which held historical design/implementation notes for
features that have already shipped.

Co-authored-by: Isaac

* chore(deps-dev): update pytest-playwright requirement

Updates the requirements on [pytest-playwright](https://github.com/microsoft/playwright-pytest) to permit the latest version.
- [Release notes](https://github.com/microsoft/playwright-pytest/releases)
- [Commits](microsoft/playwright-pytest@v0.5.0...v0.8.0)

---
updated-dependencies:
- dependency-name: pytest-playwright
  dependency-version: 0.8.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump click from 8.3.3 to 8.4.1

Bumps [click](https://github.com/pallets/click) from 8.3.3 to 8.4.1.
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.md)
- [Commits](pallets/click@8.3.3...8.4.1)

---
updated-dependencies:
- dependency-name: click
  dependency-version: 8.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump idna from 3.16 to 3.17

Bumps [idna](https://github.com/kjd/idna) from 3.16 to 3.17.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](kjd/idna@v3.16...v3.17)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.17'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump pydantic-core from 2.46.4 to 2.47.0

Bumps [pydantic-core](https://github.com/pydantic/pydantic) from 2.46.4 to 2.47.0.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/commits)

---
updated-dependencies:
- dependency-name: pydantic-core
  dependency-version: 2.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: document Omnigent host integration

Add a README section explaining the Omnigent host integration merged in #92:
what it does, how to turn it on (with the app.yaml.lakemeter overlay), the
two-credential model (SP-OAuth host tunnel + AI-Gateway harness LLM), the
runtime control endpoints, and the ENABLE_SP_APIKEYHELPER pairing. Also add
the OMNIGENTS_* / ENABLE_SP_APIKEYHELPER rows to the env-var reference table.

The feature shipped in #92 with no user-facing docs (README had zero Omnigent
mentions); this closes that gap.

* docs: correct Omnigent identity and grant guidance

* docs(auth): describe loopback SP token broker

* chore(deps): bump actions/setup-python from 6.2.0 to 7.0.0

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](actions/setup-python@a309ff8...5fda3b9)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump softprops/action-gh-release from 3.0.0 to 3.0.2

Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.0 to 3.0.2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](softprops/action-gh-release@b430933...3d0d988)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump actions/checkout from 7.0.0 to 7.0.1

Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@9c091bb...3d3c42e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump astral-sh/setup-uv from 8.1.0 to 9.0.0

Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.1.0 to 9.0.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@0880764...c771a70)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(tests): scope e2e prerequisite skip to the e2e directory

`pytest_collection_modifyitems` in tests/e2e/conftest.py receives the
whole session's item list, not just this directory's. When the e2e
prerequisites were missing (no recorded auth.json, or the databricks CLI
not authed for the profile) it marked *every* collected test as skipped
— so `uv run pytest tests/` reported "465 skipped" and the CI Tests
workflow was passing vacuously.

Filter to items that actually live under tests/e2e/. The unit suite now
runs: 459 passed, 1 skipped.

* docs: keep litellm design doc referenced by app.py/setup_opencode/setup_proxy

The plan-doc cleanup removed 2026-03-11-litellm-empty-content-blocks-design.md,
but app.py, setup_opencode.py and setup_proxy.py all point readers at it from
code comments. Restore it so those pointers don't dangle.

* chore(config): declare every ENABLE_<CLI> toggle in each app.yaml overlay

Apps overlays *replace* app.yaml rather than merging with it, and every
setup script reads its toggle as `os.environ.get("ENABLE_<CLI>", "true")`.
So a toggle omitted from an overlay is not a no-op — it silently re-enables
that CLI's install on the deployed container.

app.yaml and app.yaml.workshop listed all five; app.yaml.template and
app.yaml.lakemeter listed only ENABLE_HERMES and ENABLE_PI, so deploys from
those overlays would install Codex and Gemini even though neither has a
compatible gateway endpoint — wasted boot time plus agents that fail on
first request.

Adds tests/test_app_yaml_overlays.py to hold the invariant: every tracked
app.yaml* declares all five toggles, values stay quoted strings (unquoted
`true` parses as a bool, which the scripts' .strip().lower() would choke on),
and a new ENABLE_* appearing in setup_*.py fails the test until it's added
to the overlays.

This is the part of #30 that main hadn't already absorbed: the toggle
mechanism itself landed separately, and main's default-on policy for
Hermes/OpenCode is a deliberate later workshop decision.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: mpkrass7 <mpkrass@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dgokeeffe added a commit that referenced this pull request Aug 5, 2026
…rlay (#107)

* fix(cli-auth): atomic writes + observable failures on PAT rotation

Hermes was returning 403 ("Invalid access token") on the first call after
a PAT rotation, then succeeding on retry. Two reasons:

1. update_cli_tokens() rewrote each agent's config file with a bare
   open(path, "w"), creating a window where a concurrent Hermes
   invocation could read a half-written api_key line. Hermes is exposed
   to this because it re-reads ~/.hermes/config.yaml on every call;
   Claude/Codex/Gemini cache the token in env at process startup.
2. Every write path silently swallowed OSError, so an actual write
   failure (perms, locked file, ENOSPC) would leave the config stale
   forever with no log line — the user just saw 403s.

Adds _atomic_write_text() helper (write to .tmp, os.replace) used by
all five _update_* functions. Replaces silent except OSError: pass with
logger.warning at WARNING level. FileNotFoundError still silenced via an
explicit os.path.exists() guard so the rotator doesn't spam during the
brief window between app start and setup script completion.

Co-authored-by: Isaac

* ci(release): generate signed SBOM on each release

Adds supply-chain provenance to every GitHub Release so enterprise
security teams (PCI-DSS / ISO 27001 / APRA CPS 234) can verify what
shipped and prove it came from this repo's workflow.

What's attached to each release now:
- coda-sbom.cdx.json — CycloneDX SBOM (Python + npm deps via syft)
- coda-sbom.cdx.json.cosign.bundle — cosign keyless signature bundle
  (cert + signature + Rekor inclusion proof)

Signing uses GitHub OIDC — no long-lived keys. The signing identity is
anchored to this workflow path and the release tag, and a public
transparency-log entry is recorded in Rekor.

Workflow changes:
- Added `id-token: write` permission (required for OIDC keyless signing)
- Added anchore/sbom-action step (SHA-pinned, format=cyclonedx-json)
- Added sigstore/cosign-installer + sign-blob + in-workflow verify
- Extended softprops/action-gh-release `files:` to attach both artefacts

README changes:
- New "Verifying release provenance" subsection with the cosign
  verify-blob command operators can run.

Co-authored-by: Isaac

* docs(legal): add Project Support / AS-IS disclaimer to README

databrickslabs/dqx and other Labs projects ship a prominent "Project
Support" block disclaiming SLAs and pointing users at the LICENSE for
binding terms. CoDA has the LICENSE.md (with $1,000 aggregate liability
cap + comprehensive warranty disclaimers) and NOTICE.md (full
third-party attribution), but the README didn't surface either.

Without this block, a user evaluating CoDA could plausibly assume it's
a supported Databricks product because it lives under the
databrickslabs/ org. The disclaimer closes that gap — and is placed
immediately after the tagline (not buried at the bottom) because the
tool exposes a workspace-credential-scoped shell and AI agents that
act with full user authority.

Text lifted from databrickslabs/dqx with one grammar tweak (singular
"It is provided AS-IS" since this is one project, not multiple).
Cross-references LICENSE.md and NOTICE.md so security-conscious readers
can find the binding legal text in one hop.

Co-authored-by: Isaac

* docs: move SBOM verification to docs/SECURITY.md, drop stale plans

Keeps the README lean — release-provenance details now live in
docs/SECURITY.md where security reviewers expect them. Also removes
docs/plans/, which held historical design/implementation notes for
features that have already shipped.

Co-authored-by: Isaac

* chore(deps-dev): update pytest-playwright requirement

Updates the requirements on [pytest-playwright](https://github.com/microsoft/playwright-pytest) to permit the latest version.
- [Release notes](https://github.com/microsoft/playwright-pytest/releases)
- [Commits](microsoft/playwright-pytest@v0.5.0...v0.8.0)

---
updated-dependencies:
- dependency-name: pytest-playwright
  dependency-version: 0.8.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump click from 8.3.3 to 8.4.1

Bumps [click](https://github.com/pallets/click) from 8.3.3 to 8.4.1.
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.md)
- [Commits](pallets/click@8.3.3...8.4.1)

---
updated-dependencies:
- dependency-name: click
  dependency-version: 8.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump idna from 3.16 to 3.17

Bumps [idna](https://github.com/kjd/idna) from 3.16 to 3.17.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](kjd/idna@v3.16...v3.17)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.17'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump pydantic-core from 2.46.4 to 2.47.0

Bumps [pydantic-core](https://github.com/pydantic/pydantic) from 2.46.4 to 2.47.0.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/commits)

---
updated-dependencies:
- dependency-name: pydantic-core
  dependency-version: 2.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: document Omnigent host integration

Add a README section explaining the Omnigent host integration merged in #92:
what it does, how to turn it on (with the app.yaml.lakemeter overlay), the
two-credential model (SP-OAuth host tunnel + AI-Gateway harness LLM), the
runtime control endpoints, and the ENABLE_SP_APIKEYHELPER pairing. Also add
the OMNIGENTS_* / ENABLE_SP_APIKEYHELPER rows to the env-var reference table.

The feature shipped in #92 with no user-facing docs (README had zero Omnigent
mentions); this closes that gap.

* docs: correct Omnigent identity and grant guidance

* docs(auth): describe loopback SP token broker

* chore(deps): bump actions/setup-python from 6.2.0 to 7.0.0

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](actions/setup-python@a309ff8...5fda3b9)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump softprops/action-gh-release from 3.0.0 to 3.0.2

Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.0 to 3.0.2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](softprops/action-gh-release@b430933...3d0d988)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump actions/checkout from 7.0.0 to 7.0.1

Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@9c091bb...3d3c42e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump astral-sh/setup-uv from 8.1.0 to 9.0.0

Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.1.0 to 9.0.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@0880764...c771a70)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(tests): scope e2e prerequisite skip to the e2e directory

`pytest_collection_modifyitems` in tests/e2e/conftest.py receives the
whole session's item list, not just this directory's. When the e2e
prerequisites were missing (no recorded auth.json, or the databricks CLI
not authed for the profile) it marked *every* collected test as skipped
— so `uv run pytest tests/` reported "465 skipped" and the CI Tests
workflow was passing vacuously.

Filter to items that actually live under tests/e2e/. The unit suite now
runs: 459 passed, 1 skipped.

* docs: keep litellm design doc referenced by app.py/setup_opencode/setup_proxy

The plan-doc cleanup removed 2026-03-11-litellm-empty-content-blocks-design.md,
but app.py, setup_opencode.py and setup_proxy.py all point readers at it from
code comments. Restore it so those pointers don't dangle.

* chore(config): declare every ENABLE_<CLI> toggle in each app.yaml overlay

Apps overlays *replace* app.yaml rather than merging with it, and every
setup script reads its toggle as `os.environ.get("ENABLE_<CLI>", "true")`.
So a toggle omitted from an overlay is not a no-op — it silently re-enables
that CLI's install on the deployed container.

app.yaml and app.yaml.workshop listed all five; app.yaml.template and
app.yaml.lakemeter listed only ENABLE_HERMES and ENABLE_PI, so deploys from
those overlays would install Codex and Gemini even though neither has a
compatible gateway endpoint — wasted boot time plus agents that fail on
first request.

Adds tests/test_app_yaml_overlays.py to hold the invariant: every tracked
app.yaml* declares all five toggles, values stay quoted strings (unquoted
`true` parses as a bool, which the scripts' .strip().lower() would choke on),
and a new ENABLE_* appearing in setup_*.py fails the test until it's added
to the overlays.

This is the part of #30 that main hadn't already absorbed: the toggle
mechanism itself landed separately, and main's default-on policy for
Hermes/OpenCode is a deliberate later workshop decision.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: mpkrass7 <mpkrass@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dgokeeffe added a commit that referenced this pull request Aug 5, 2026
…able Flask key (#40)

* fix(cli-auth): atomic writes + observable failures on PAT rotation

Hermes was returning 403 ("Invalid access token") on the first call after
a PAT rotation, then succeeding on retry. Two reasons:

1. update_cli_tokens() rewrote each agent's config file with a bare
   open(path, "w"), creating a window where a concurrent Hermes
   invocation could read a half-written api_key line. Hermes is exposed
   to this because it re-reads ~/.hermes/config.yaml on every call;
   Claude/Codex/Gemini cache the token in env at process startup.
2. Every write path silently swallowed OSError, so an actual write
   failure (perms, locked file, ENOSPC) would leave the config stale
   forever with no log line — the user just saw 403s.

Adds _atomic_write_text() helper (write to .tmp, os.replace) used by
all five _update_* functions. Replaces silent except OSError: pass with
logger.warning at WARNING level. FileNotFoundError still silenced via an
explicit os.path.exists() guard so the rotator doesn't spam during the
brief window between app start and setup script completion.

Co-authored-by: Isaac

* sec(enterprise): SECURITY.md + auth-leak fix + telemetry opt-out + stable Flask key

Bundle of four enterprise-readiness improvements from an independent
security review against NAB (APRA / CPS 234) and Coles (PCI-DSS / ISO
27001) procurement requirements.

E-2: Add .github/SECURITY.md (P0 compliance blocker)
================================================================
Vendor security assessments (SIG / CAIQ) ask "do you have a documented
vulnerability disclosure process?" — answer was no. Now we have:
  - Two private disclosure channels (GitHub Security Advisories + email)
  - Acknowledgement / triage / fix-plan / disclosure timeline
  - Severity-based patch SLA (Critical: 7d, High: 14d, Medium: 30d)
  - Scope boundaries (in/out of scope)
  - Supply-chain controls summary for procurement reviewers
  - Pointer to docs/enterprise.md § known limits for the trade-offs
    we've deliberately accepted

E-4: Remove /api/app-state from auth-exempt list (P1)
================================================================
app.py:808 — /api/app-state was in the same exempt list as /health.
Unauthenticated callers could fetch app_owner email + last_rotation_iso
+ owner_resolved_at, which is enough to fingerprint session timing and
identify the workspace owner.

The endpoint has no pre-auth use case in the UI (the polling endpoints
the UI needs before PAT setup are /api/setup-status and /api/pat-status,
both still exempt). Removed from the list.

Tests:
  - test_app_state_denied_for_non_owner — non-owners get 403
  - test_app_state_allowed_for_owner — owner still gets 200

E-9: Telemetry opt-out via CODA_TELEMETRY_DISABLED (P2)
================================================================
telemetry.py — log_telemetry() now short-circuits when
CODA_TELEMETRY_DISABLED is truthy. Default behaviour unchanged (telemetry
still on by default).

Why this matters for procurement: regulated customers (banks, retailers
with PCI-DSS) must inventory every outbound data flow from their
workspace boundary. CoDA's User-Agent-header telemetry to Databricks
servers was previously undisclosed and unsuppressible. Opt-out gives
those customers a clean answer for their third-party-risk register.

app.yaml gets a commented `CODA_TELEMETRY_DISABLED: "true"` example so
operators see the knob. 5 new unit tests cover truthy/falsy parsing and
that the background thread does NOT spawn when disabled.

E-11: Stable Flask secret_key via FLASK_SECRET_KEY (P2)
================================================================
app.py:56 used to be `app.secret_key = os.urandom(24)` — regenerated on
every worker restart, invalidating all Flask session cookies. With
single-worker config today this is mostly cosmetic (sessions get torn
down anyway when PTYs die with the worker), but it's a flagged finding
in any key-management audit and would actively break sessions if we
ever scaled to multi-worker.

Now reads FLASK_SECRET_KEY from env (typically wired to a Databricks
secret in app.yaml). Falls back to os.urandom for local dev with a
WARNING log line so operators see when production is using an ephemeral
key. app.yaml gets a commented example.

Test results
================================================================
- 244/244 unit tests pass
- 42/42 in the auth-enforcement + telemetry-opt-out + app-state suites
- Default behaviour unchanged when no new env vars are set

Out of scope (separate PRs / future work)
================================================================
- SBOM generation in the release workflow — PR #41
- Rate limiting on /api/configure-pat (E-3) — needs slowapi wire-up
- Session-level command audit trail (E-8) — medium effort, structured logs
- CSRF protection on /api/input (E-14) — medium effort
- `requests = git+https://...` PyPI proxy bypass (E-6) — blocked by
  Databricks internal PyPI proxy availability

Co-authored-by: Isaac

* ci(release): generate signed SBOM on each release

Adds supply-chain provenance to every GitHub Release so enterprise
security teams (PCI-DSS / ISO 27001 / APRA CPS 234) can verify what
shipped and prove it came from this repo's workflow.

What's attached to each release now:
- coda-sbom.cdx.json — CycloneDX SBOM (Python + npm deps via syft)
- coda-sbom.cdx.json.cosign.bundle — cosign keyless signature bundle
  (cert + signature + Rekor inclusion proof)

Signing uses GitHub OIDC — no long-lived keys. The signing identity is
anchored to this workflow path and the release tag, and a public
transparency-log entry is recorded in Rekor.

Workflow changes:
- Added `id-token: write` permission (required for OIDC keyless signing)
- Added anchore/sbom-action step (SHA-pinned, format=cyclonedx-json)
- Added sigstore/cosign-installer + sign-blob + in-workflow verify
- Extended softprops/action-gh-release `files:` to attach both artefacts

README changes:
- New "Verifying release provenance" subsection with the cosign
  verify-blob command operators can run.

Co-authored-by: Isaac

* docs(legal): add Project Support / AS-IS disclaimer to README

databrickslabs/dqx and other Labs projects ship a prominent "Project
Support" block disclaiming SLAs and pointing users at the LICENSE for
binding terms. CoDA has the LICENSE.md (with $1,000 aggregate liability
cap + comprehensive warranty disclaimers) and NOTICE.md (full
third-party attribution), but the README didn't surface either.

Without this block, a user evaluating CoDA could plausibly assume it's
a supported Databricks product because it lives under the
databrickslabs/ org. The disclaimer closes that gap — and is placed
immediately after the tagline (not buried at the bottom) because the
tool exposes a workspace-credential-scoped shell and AI agents that
act with full user authority.

Text lifted from databrickslabs/dqx with one grammar tweak (singular
"It is provided AS-IS" since this is one project, not multiple).
Cross-references LICENSE.md and NOTICE.md so security-conscious readers
can find the binding legal text in one hop.

Co-authored-by: Isaac

* fix(security): close unauth info-disclosure surface (setup-status, pat-status, app-state, /health)

Closes the three P3/P4 info-disclosure findings from the 2026-05-17
pen test:

1. /api/setup-status was auth-exempt and returned the full setup_state
   dict including step error messages (stderr trimmed to 500 chars). If a
   setup script failed with leaky stderr — paths, version strings, env-var
   echoes — anyone hitting the URL could read it.

2. /api/pat-status was auth-exempt and returned workspace_host. Already
   known to anyone who reached the URL (they're auth'd to the same
   workspace), but principle-of-least-info says trim it.

3. /api/app-state was auth-exempt and returned app_owner email + last
   PAT rotation timestamp. PR #40 also removes this — including the same
   one-line fix here lets it ship sooner; PR #40 will rebase cleanly.

4. /health was auth-exempt and returned version, active_sessions,
   setup_status, session_timeout_seconds. Version exposure enables
   CVE-targeted exploit selection. Trimmed to {"status": "healthy"} —
   no internal or external caller needs the rich shape (the frontend
   doesn't poll /health; the Apps platform uses its own liveness
   mechanism upstream of the app).

Removed setup-status, pat-status, and app-state from the
before_request auth-exempt list. The frontend continues to poll all
three because it loads from / (auth'd), so it already has SSO cookies.

Test changes:
- 5 new tests in TestInfoDisclosureEndpoints covering: setup-status
  denied/allowed, pat-status denied, app-state denied, /health
  minimal-response anti-leak (explicit assertions that version,
  setup_status, active_sessions are NOT in the response).
- Updated test_session_linger.py: the 24h timeout assertion now reads
  SESSION_TIMEOUT_SECONDS from the module directly instead of via
  /health, since exposing the value to unauth callers was the leak
  we're closing. Stronger test, smaller attack surface.

236/236 unit tests pass.

Co-authored-by: Isaac

* docs: move SBOM verification to docs/SECURITY.md, drop stale plans

Keeps the README lean — release-provenance details now live in
docs/SECURITY.md where security reviewers expect them. Also removes
docs/plans/, which held historical design/implementation notes for
features that have already shipped.

Co-authored-by: Isaac

* chore(deps-dev): update pytest-playwright requirement

Updates the requirements on [pytest-playwright](https://github.com/microsoft/playwright-pytest) to permit the latest version.
- [Release notes](https://github.com/microsoft/playwright-pytest/releases)
- [Commits](microsoft/playwright-pytest@v0.5.0...v0.8.0)

---
updated-dependencies:
- dependency-name: pytest-playwright
  dependency-version: 0.8.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump click from 8.3.3 to 8.4.1

Bumps [click](https://github.com/pallets/click) from 8.3.3 to 8.4.1.
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.md)
- [Commits](pallets/click@8.3.3...8.4.1)

---
updated-dependencies:
- dependency-name: click
  dependency-version: 8.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump idna from 3.16 to 3.17

Bumps [idna](https://github.com/kjd/idna) from 3.16 to 3.17.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](kjd/idna@v3.16...v3.17)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.17'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump pydantic-core from 2.46.4 to 2.47.0

Bumps [pydantic-core](https://github.com/pydantic/pydantic) from 2.46.4 to 2.47.0.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/commits)

---
updated-dependencies:
- dependency-name: pydantic-core
  dependency-version: 2.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: document Omnigent host integration

Add a README section explaining the Omnigent host integration merged in #92:
what it does, how to turn it on (with the app.yaml.lakemeter overlay), the
two-credential model (SP-OAuth host tunnel + AI-Gateway harness LLM), the
runtime control endpoints, and the ENABLE_SP_APIKEYHELPER pairing. Also add
the OMNIGENTS_* / ENABLE_SP_APIKEYHELPER rows to the env-var reference table.

The feature shipped in #92 with no user-facing docs (README had zero Omnigent
mentions); this closes that gap.

* docs: correct Omnigent identity and grant guidance

* docs(auth): describe loopback SP token broker

* chore(deps): bump actions/setup-python from 6.2.0 to 7.0.0

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](actions/setup-python@a309ff8...5fda3b9)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump softprops/action-gh-release from 3.0.0 to 3.0.2

Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.0 to 3.0.2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](softprops/action-gh-release@b430933...3d0d988)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump actions/checkout from 7.0.0 to 7.0.1

Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@9c091bb...3d3c42e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump astral-sh/setup-uv from 8.1.0 to 9.0.0

Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.1.0 to 9.0.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@0880764...c771a70)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(tests): scope e2e prerequisite skip to the e2e directory

`pytest_collection_modifyitems` in tests/e2e/conftest.py receives the
whole session's item list, not just this directory's. When the e2e
prerequisites were missing (no recorded auth.json, or the databricks CLI
not authed for the profile) it marked *every* collected test as skipped
— so `uv run pytest tests/` reported "465 skipped" and the CI Tests
workflow was passing vacuously.

Filter to items that actually live under tests/e2e/. The unit suite now
runs: 459 passed, 1 skipped.

* docs: keep litellm design doc referenced by app.py/setup_opencode/setup_proxy

The plan-doc cleanup removed 2026-03-11-litellm-empty-content-blocks-design.md,
but app.py, setup_opencode.py and setup_proxy.py all point readers at it from
code comments. Restore it so those pointers don't dangle.

* chore(config): declare every ENABLE_<CLI> toggle in each app.yaml overlay

Apps overlays *replace* app.yaml rather than merging with it, and every
setup script reads its toggle as `os.environ.get("ENABLE_<CLI>", "true")`.
So a toggle omitted from an overlay is not a no-op — it silently re-enables
that CLI's install on the deployed container.

app.yaml and app.yaml.workshop listed all five; app.yaml.template and
app.yaml.lakemeter listed only ENABLE_HERMES and ENABLE_PI, so deploys from
those overlays would install Codex and Gemini even though neither has a
compatible gateway endpoint — wasted boot time plus agents that fail on
first request.

Adds tests/test_app_yaml_overlays.py to hold the invariant: every tracked
app.yaml* declares all five toggles, values stay quoted strings (unquoted
`true` parses as a bool, which the scripts' .strip().lower() would choke on),
and a new ENABLE_* appearing in setup_*.py fails the test until it's added
to the overlays.

This is the part of #30 that main hadn't already absorbed: the toggle
mechanism itself landed separately, and main's default-on policy for
Hermes/OpenCode is a deliberate later workshop decision.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: mpkrass7 <mpkrass@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant